require 'tempfile'

class Job < ActiveRecord::Base

  include MolFileValidator
  include MSFileValidator

  belongs_to :user
  has_many :fragments, :order => "fragment", :dependent => :destroy

  named_scope :completed, { :conditions => { :completed => true }}
  default_scope :order => 'created_at DESC'

  validates_presence_of :mol_file, :ms_file, :measurement_error, :intensity_threshold, :rr, :cuts
  validates_numericality_of :cuts, :only_integer => true, :greater_than_or_equal_to => 0
  validates_numericality_of :measurement_error, :greater_than_or_equal_to => 0, :less_than_or_equal_to => 1
  validates_inclusion_of :rr, :in => [0, 1]
  validates_mol_file_structure
  validates_ms_file_structure

  attr_accessible :mol_file, :ms_file, :measurement_error, :intensity_threshold, :rr, :cuts, :description, :error_units
  attr_accessor :error_units

  before_validation_on_create :read_mol_file_from_tempfile, :if => lambda { |job| job.mol_file.is_a? Tempfile }
  before_validation_on_create :extract_formula_from_mol_file, :if => lambda { |job| job.formula.nil? }
  before_validation_on_create :convert_measurement_error_from_ppm_to_mda, :if => lambda { |job| job.error_units == "ppm" }

  # Selects the fragments specified by fragment_ids.
  def update_selected_fragments(fragment_ids)
    Fragment.transaction do
      fragments.selected.update_all(:selected => false)
      fragments.update_all({ :selected => true }, { :id => fragment_ids })
    end
  end

  # Returns distinct peaknums of associated fragments.
  def peaknums
    Fragment.all(:select => "DISTINCT peaknum", :conditions => { :job_id => id },
                 :order => :peaknum).map(&:peaknum)
  end

  # Returns an array with the peaks of current job.
  def peaks
    selected_peaks = fragments.selected.map(&:peaknum)
    calculated_peaks = peaknums
    peaks = ms_file.each_line.sort{ |a, b| a.split(/\s/).first.to_f <=> b.split(/\s/).first.to_f }
    peaks.each_with_index.map do |line, line_number|
      mass, intensity = line.split(/\s+/)
      {
        :mass => mass,
        :intensity => intensity,
        :state => selected_peaks.include?(line_number + 1) ? "Selected" : calculated_peaks.include?(line_number + 1) ? "Found" : "Not found",
        :num => line_number + 1
      }
    end
  end

  # Returns the fragment candidates for each peak in the order and quantity
  # specified by the o<peaknum> and peak-<peaknum> parameters.
  def fragment_candidates(params)
    # The union plugin can't handle the case where there is only one union
    # group, so we have to check the arity.
    if peaknums.size > 1
      fragments.union(peaknums.map { |peaknum|
                        {
                          :conditions => { :peaknum => peaknum },
                          :limit => parse_limit(params["peak-#{peaknum}"]),
                          :order => parse_order(params["o#{peaknum}"])
                        }
                      }, :order => :peaknum)
    else
      fragments.find(:all, :conditions => { :peaknum => peaknums.first },
                     :limit => parse_limit(params["peak-#{peaknums.first}"]),
                     :order => parse_order(params["o#{peaknums.first}"]))
    end
  end

  private

  def parse_order(order)
    case order
    when /(\w+)A/ then "#{$1} ASC"
    when /(\w+)D/ then "#{$1} DESC"
    else nil # The default ordering is implicitly 'fragment ASC'
    end
  end

  def parse_limit(limit)
    limit.to_i > 0 ? limit : 3
  end

  def read_mol_file_from_tempfile
    self.mol_file = mol_file.read
  end

  def convert_measurement_error_from_ppm_to_mda
    molecule_mass = self.formula.scan(/([A-z][a-z]?)(\d*)/).map { |symbol, count|
      APP_CONFIG['atomic_weights'][symbol] * (!count.empty? ? count.to_i : 1)
    }.sum
    self.measurement_error = ((self.measurement_error * (molecule_mass / (1_000_000))) *
                              (1000)).round(4)
  rescue
    # If the formula cannot be extracted from molfile.
  end

  # Extracts molecule formula from molfile.
  def extract_formula_from_mol_file
    lines = self.mol_file.split(/\r?\n|\r/)
    number_of_atoms = lines[3][0..2].to_i
    number_of_bonds = lines[3][3..5].to_i
    atom_lines = (4 .. 4 + number_of_atoms - 1)
    bond_lines = (4 + number_of_atoms .. 4 + number_of_atoms + number_of_bonds - 1)

    # Array of atom symbols extracted from atom lines.
    symbols = lines[atom_lines].map { |atomline| atomline[31..33].strip }

    # The molfile specification defines the hhh field to contain the value
    # hydrogen count - 1, i.e the minimum value would be 1. Certain molfile
    # implementations seem leave this field as 0. In these cases we attempt to
    # infer the number of hydrogen atoms by counting the unsatisfied bonds.
    number_of_hydrogen_atoms = lines[atom_lines].map { |atomline|
      hydrogen_count = atomline[42..44].to_i; hydrogen_count.to_i > 0 ? hydrogen_count.to_i - 1 : 0
    }.sum

    unless number_of_hydrogen_atoms > 0 then
      # TODO Support for bond types 4-8.
      # (fid does not seem to handle them either.)
      number_of_unsatisfied_bonds = lines[bond_lines].map { |bondline|
        type = bondline[6..8].to_i
        (0..3).include?(type) ? type : 1
      }.sum

      # Infer total number of atoms from known bond counts.
      total_number_of_atoms = symbols.map { |symbol|
        APP_CONFIG['bond_counts'][symbol] || 0
      }.sum

      # The difference between total number of atoms and atoms accounted for
      # gives us the number of hydrogen atoms in the molecule.
      number_of_hydrogen_atoms = total_number_of_atoms -
        ((number_of_unsatisfied_bonds * 2) - symbols.count("H"))

      # Add number of hydrogens matching the total atom charges.
      # This seems to fix the discrepancy with fid and kegg database.
      charge_value = { 0 => 0, 1 => 3, 2 => 2, 3 => 1, 4 => 0, 5 => -1, 6 => -2, 7 => -1 }
      number_of_hydrogen_atoms += lines[atom_lines].map { |atomline|
        charge_value[atomline[36..38].to_i]
      }.sum
    end

    # Add hydrogen atoms (if any) to symbol array and build formula string.
    symbols += ['H'] * number_of_hydrogen_atoms unless number_of_hydrogen_atoms <= 0
    formula = Hash[symbols.map { |symbol| [symbol, symbols.count("#{symbol}")] }].sort.join

    # Order symbols according to Hill system.
    if formula.sub!(/(C\d+)/, '') then
      carbon = $1
      if formula.sub!(/(H\d+)/, '') then
        formula = $1 + formula
      end
      formula = carbon + formula
    end

    # Get rid number after single atoms and update formula.
    self.formula = formula.gsub(/([A-Za-z])1(?!\d)/, '\1')
  rescue
    # Pass
  end
end
